/*
 * commons is a based project implemented
 * Copyright (C) 2024 Jasmine
 */
package com.easy.log.interceptor;

import java.time.Clock;
import java.util.Objects;
import java.util.Set;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import com.alibaba.ttl.TransmittableThreadLocal;
import com.easy.core.utils.ApplicationContextHolder;
import com.easy.log.anno.SysLog;
import com.easy.log.dao.SysLogDao;
import com.easy.log.domain.LogRecord;
import com.easy.log.enums.Include;
import com.easy.log.http.RecordableServletHttpRequest;
import com.easy.log.http.RecordableServletHttpResponse;
import com.easy.log.props.LogProperties;

import io.swagger.v3.oas.annotations.Hidden;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

/**
 * 日志拦截器
 *
 * @author Jasmine
 * @since 1.0.0
 */
public class LogInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(LogInterceptor.class);
    private final SysLogDao sysLogDao;
    private final LogProperties logProperties;
    private final TransmittableThreadLocal<LogRecord.Started> timestampTtl = new TransmittableThreadLocal<>();

    public LogInterceptor(SysLogDao sysLogDao, LogProperties logProperties) {
        this.sysLogDao = sysLogDao;
        this.logProperties = logProperties;
    }

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
        @NonNull Object handler) {
        Clock timestamp = Clock.systemUTC();
        if (this.isRequestRecord(handler, request)) {
            LogRecord.Started startedLogRecord = LogRecord.start(timestamp, new RecordableServletHttpRequest(request));
            timestampTtl.set(startedLogRecord);
        }
        return true;
    }

    @Override
    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
        @NonNull Object handler, Exception e) {
        LogRecord.Started startedLogRecord = timestampTtl.get();
        if (null == startedLogRecord) {
            return;
        }
        timestampTtl.remove();
        try {
            HandlerMethod handlerMethod = (HandlerMethod)handler;

            SysLog methodLog = handlerMethod.getMethodAnnotation(SysLog.class);
            SysLog classLog = handlerMethod.getBeanType().getDeclaredAnnotation(SysLog.class);
            Set<Include> includeSet = this.getIncludes(methodLog, classLog);

            LogRecord finishedLogRecord =
                startedLogRecord.finish(new RecordableServletHttpResponse(response, response.getStatus()), includeSet);

            finishedLogRecord.getRequest().setMethod(handlerMethod.toString());

            // 记录所属模块
            if (includeSet.contains(Include.MODULE)) {
                this.logModule(finishedLogRecord, handlerMethod);
            }

            // 记录日志描述
            if (includeSet.contains(Include.DESC)) {
                this.logDescription(finishedLogRecord, handlerMethod);
            }

            // 保存记录
            if (Objects.nonNull(sysLogDao)) {
                sysLogDao.add(finishedLogRecord);
            }
        } catch (Exception ex) {
            log.error("Logging http log occurred an error: {}.", ex.getMessage(), ex);
        }
    }

    /**
     * 是否要记录日志
     *
     * @param handler 处理器
     * @param request 请求对象
     * @return true：需要记录；false：不需要记录
     */
    private boolean isRequestRecord(Object handler, HttpServletRequest request) {
        if (!(handler instanceof HandlerMethod handlerMethod)) {
            return false;
        }

        Class<?> handlerBeanType = handlerMethod.getBeanType();

        // 如果接口方法或类上有 @SysLog 注解，且要求忽略该接口，则不记录日志
        SysLog methodLog = handlerMethod.getMethodAnnotation(SysLog.class);
        if (null != methodLog && methodLog.ignore()) {
            return false;
        }
        SysLog classLog = handlerBeanType.getDeclaredAnnotation(SysLog.class);
        if (null != classLog && classLog.ignore()) {
            return false;
        }

        // 不拦截 /error
        ServerProperties serverProperties = ApplicationContextHolder.getBean(ServerProperties.class);
        if (request.getRequestURI().equals(serverProperties.getError().getPath())) {
            return false;
        }

        // 如果接口被隐藏，不记录日志
        if (null != handlerBeanType.getDeclaredAnnotation(Hidden.class)) {
            return false;
        }
        Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
        if (null != methodOperation && methodOperation.hidden()) {
            return false;
        }
        Hidden methodHidden = handlerMethod.getMethodAnnotation(Hidden.class);
        if (null != methodHidden) {
            return false;
        }
        return true;
    }

    /**
     * 获取日志包含信息
     *
     * @param methodLog 方法级 Log 注解
     * @param classLog 类级 Log 注解
     * @return 日志包含信息
     */
    private Set<Include> getIncludes(SysLog methodLog, SysLog classLog) {
        Set<Include> includeSet = logProperties.getIncludes();
        if (null != classLog) {
            this.processInclude(includeSet, classLog);
        }
        if (null != methodLog) {
            this.processInclude(includeSet, methodLog);
        }
        return includeSet;
    }

    /**
     * 处理日志包含信息
     *
     * @param includes 日志包含信息
     * @param logAnnotation Log 注解
     */
    private void processInclude(Set<Include> includes, SysLog logAnnotation) {
        Include[] includeArr = logAnnotation.includes();
        if (includeArr.length > 0) {
            includes.addAll(Set.of(includeArr));
        }
        Include[] excludeArr = logAnnotation.excludes();
        if (excludeArr.length > 0) {
            includes.removeAll(Set.of(excludeArr));
        }
    }

    /**
     * 记录模块
     *
     * @param logRecord 日志信息
     * @param handlerMethod 处理器方法
     */
    private void logModule(LogRecord logRecord, HandlerMethod handlerMethod) {
        // 优先级： 接口方法上的 @SysLog(module = "模块") > 接口类上的 @SysLog(module = "模块") > @Tag(name = "模块") 内容

        // 例如：@Log(module = "部门管理") -> 部门管理
        SysLog methodLog = handlerMethod.getMethodAnnotation(SysLog.class);
        if (Objects.nonNull(methodLog) && StringUtils.isNotBlank(methodLog.module())) {
            logRecord.setModule(methodLog.module());
            return;
        }

        SysLog classLog = handlerMethod.getBeanType().getDeclaredAnnotation(SysLog.class);
        if (Objects.nonNull(classLog) && StringUtils.isNotBlank(classLog.module())) {
            logRecord.setModule(classLog.module());
            return;
        }

        // 例如：@Tag(name = "部门管理") -> 部门管理
        Tag classTag = handlerMethod.getBeanType().getDeclaredAnnotation(Tag.class);
        if (Objects.nonNull(classTag) && StringUtils.isNotBlank(classTag.name())) {
            logRecord.setModule(classTag.name());
        } else {
            logRecord.setModule("请在该接口类上指定所属模块");
        }
    }

    /**
     * 记录描述
     *
     * @param logRecord 日志信息
     * @param handlerMethod 处理器方法
     */
    private void logDescription(LogRecord logRecord, HandlerMethod handlerMethod) {
        // 优先级： 接口类上的 @SysLog(desc = "新增部门") > @Operation(summary="新增部门")

        // 例如：@Log("新增部门") -> 新增部门
        SysLog methodLog = handlerMethod.getMethodAnnotation(SysLog.class);
        if (Objects.nonNull(methodLog) && StringUtils.isNotBlank(methodLog.desc())) {
            logRecord.setDesc(methodLog.desc());
            return;
        }

        // 例如：@Operation(summary="新增部门") -> 新增部门
        Operation methodOperation = handlerMethod.getMethodAnnotation(Operation.class);
        if (Objects.nonNull(methodOperation) && StringUtils.isNotBlank(methodOperation.summary())) {
            logRecord.setDesc(methodOperation.summary());
        } else {
            logRecord.setDesc("请在该接口方法上指定日志描述");
        }
    }

}
